#ifndef SINK_HPP
#define SINK_HPP
#include <mutex>
#include <memory>
#include <string>
#include <fstream>
#include <sstream>
#include <iomanip>
#include <iostream>
#include <sys/stat.h>
#include <cassert>
#include <chrono>

namespace ly
{
    class Util
    {
    public:
        static void create_directory(const std::string &filename)
        {
            size_t cur = filename.find_first_of("/\\");
            while (cur != std::string::npos)
            {
                mkdir(filename.substr(0, cur).c_str(), 0755);
                cur = filename.find_first_of("/\\", cur + 1);
            }
        }

        static std::string get_cur_time()
        {
            auto now = std::time(nullptr);
            auto tm = *std::localtime(&now);

            std::ostringstream ss;
            ss << std::put_time(&tm, "%Y%m%d_%H%M%S_");

            auto duration = std::chrono::high_resolution_clock::now().time_since_epoch();
            auto microseconds = std::chrono::duration_cast<std::chrono::microseconds>(duration).count();
            ss << std::setfill('0') << std::setw(6) << microseconds % 1000000;
            return ss.str();
        }

        static std::string get_new_name(const std::string &filename)
        {
            size_t pos_dot = filename.find_last_of(".");
            std::string l_name = pos_dot == std::string::npos ? filename : filename.substr(0, pos_dot);
            std::string r_name = pos_dot == std::string::npos ? "" : filename.substr(pos_dot);
            std::string time = get_cur_time();
            std::stringstream ss;
            ss << l_name << "_" << time << r_name;
            return ss.str();
        }
    };

    class Sink
    {
    public:
        using ptr = std::shared_ptr<Sink>;
        virtual void log(const char *str, size_t len) = 0;
        virtual ~Sink() = default;
    protected:
        std::mutex _mutex;
    };

    class StdoutSink : public Sink
    {
    public:
        virtual void log(const char *str, size_t len) override
        {
            std::lock_guard<std::mutex> lock(_mutex);
            fwrite(str, sizeof(char), len, stdout);
        }
    };

    class FileSink : public Sink
    {
    public:
        FileSink(const std::string &filename)
            : _filename(filename)
        {
            Util::create_directory(filename);
            choose_file_unsave(filename);
        }

        virtual void log(const char *str, size_t len) override
        {
            std::lock_guard<std::mutex> lock(_mutex);
            log_unsave(str, len);
        }

    protected:
        void choose_file_unsave(const std::string &filename)
        {
            if (_ofs.is_open())
                _ofs.close();
            _ofs.open(filename, std::ios_base::binary | std::ios_base::app);
            if (!_ofs)
                std::cout << filename << " open error" << std::endl;
        }

        void log_unsave(const char *str, size_t len)
        {
            _ofs.write(str, len);
            if (!_ofs)
                std::cout << _filename << " write error" << std::endl;
        }
    protected:
        std::string _filename;
        std::ofstream _ofs;
    };

    class SizeRollSink : public FileSink
    {
    public:
        SizeRollSink(const std::string &filename, size_t limit_size = 1024 * 1024 * 1024)
            : FileSink(Util::get_new_name(filename)), _limit_size(limit_size), _cur_size(0), _org_filename(filename)
        {
            assert(limit_size);
        }

        virtual void log(const char *str, size_t len) override
        {
            std::lock_guard<std::mutex> lock(_mutex);
            FileSink::log_unsave(str, len);
            _cur_size += len;
            if (_cur_size >= _limit_size)
            {
                choose_file_unsave(Util::get_new_name(_org_filename));
                _cur_size = 0;
            }
        }

    private:
        size_t _limit_size;
        size_t _cur_size;
        std::string _org_filename;
    };

    class TimeRollSink : public FileSink
    {
    public:
        TimeRollSink(const std::string &filename, time_t limit_time)
            : FileSink(Util::get_new_name(filename)), _limit_time(limit_time), _prev_time(time(0)), _org_filename(filename)
        {
            assert(limit_time);
        }

        virtual void log(const char *str, size_t len) override
        {
            std::lock_guard<std::mutex> lock(_mutex);
            if (time(0) - _prev_time >= _limit_time)
            {
                choose_file_unsave(Util::get_new_name(_org_filename));
                _prev_time = time(0);
            }
            FileSink::log_unsave(str, len);
        }

    private:
        time_t _limit_time;
        time_t _prev_time;
        std::string _org_filename;
    };

    class SinkFactory
    {
    public:
        template <typename T, typename... Args>
        static Sink::ptr create(Args &&...args)
        {
            return std::make_shared<T>(std::forward<Args>(args)...);
        }

        static Sink::ptr create(const std::string &type, const std::unordered_map<std::string, std::string> &config)
        {
            if (type == "stdout")
                return std::make_shared<StdoutSink>();
            if (type == "file")
                return std::make_shared<FileSink>(config.at("filename"));
            if (type == "sizeroll")
                return std::make_shared<SizeRollSink>(config.at("filename"), std::stoi(config.at("limit_size")));
            if (type == "timeroll")
                return std::make_shared<TimeRollSink>(config.at("filename"), std::stoi(config.at("limit_time")));
            return nullptr;
        }
    };

} // namespace ly

#endif // #define SINK_HPP